🔐 Soluzioni Protette

Per accedere alle soluzioni degli esercizi, inserisci la password fornita dal docente.

❌ Password errata! Riprova.

💪 Esercitazione OOP in Python

5 Esercizi Progressivi per Mettere in Pratica i Concetti Appresi

📋 Informazioni sull'Esercitazione

🎯 Obiettivi dell'Esercitazione

Questa esercitazione ti guiderà attraverso 5 esercizi progressivi che coprono tutti i concetti fondamentali della Programmazione Orientata agli Oggetti in Python che hai studiato nella lezione.

Gli esercizi sono ordinati per difficoltà crescente:

  • Esercizio 1-2: Concetti base (classi, __init__, metodi)
  • Esercizio 3: Property e incapsulamento
  • Esercizio 4: Ereditarietà e polimorfismo
  • Esercizio 5: Integrazione di più concetti

💡 Come Affrontare gli Esercizi

  1. Leggi attentamente la traccia e i requisiti
  2. Pianifica prima di scrivere codice
  3. Testa il tuo codice con gli esempi forniti
  4. Usa gli hint se rimani bloccato
  5. Confronta la tua soluzione con quella proposta solo dopo aver provato
  6. Non copiare le soluzioni: impari solo facendo!

🔐 Nota: Le soluzioni sono protette da password. Quando clicchi su "Mostra/Nascondi Soluzione", ti verrà chiesta la password fornita dal docente.

1

Classe Studente

⭐ Facile

📝 Traccia

Crea una classe Studente che rappresenti uno studente universitario. La classe deve permettere di memorizzare informazioni sullo studente e gestire i suoi voti.

✅ Requisiti

  1. La classe deve avere un metodo __init__ che accetta:
    • nome (stringa)
    • cognome (stringa)
    • matricola (stringa)
  2. Deve avere un attributo voti che è una lista vuota (inizializzata automaticamente)
  3. Implementa un metodo aggiungi_voto(voto) che aggiunge un voto alla lista (il voto deve essere tra 18 e 30)
  4. Implementa un metodo media_voti() che restituisce la media dei voti (0 se non ci sono voti)
  5. Implementa il metodo __str__ che restituisce una stringa del tipo: "Studente: Mario Rossi (Matricola: 12345) - Media: 27.5"
Esempio di utilizzo:
studente = Studente("Mario", "Rossi", "12345")
studente.aggiungi_voto(28)
studente.aggiungi_voto(30)
studente.aggiungi_voto(25)
print(studente)  # Studente: Mario Rossi (Matricola: 12345) - Media: 27.67
print(f"Media: {studente.media_voti()}")  # Media: 27.666666666666668
                    

💡 Suggerimenti

  • Ricorda di usare self come primo parametro in tutti i metodi
  • Inizializza la lista voti nel metodo __init__
  • Per calcolare la media usa sum(self.voti) / len(self.voti)
  • Controlla che ci siano voti prima di calcolare la media (evita divisione per zero!)
  • Per validare il voto usa if 18 <= voto <= 30:

✅ Soluzione Proposta

class Studente:
    """Rappresenta uno studente universitario con i suoi voti."""
    
    def __init__(self, nome, cognome, matricola):
        """
        Inizializza uno studente.
        
        Args:
            nome (str): Nome dello studente
            cognome (str): Cognome dello studente
            matricola (str): Matricola univoca
        """
        self.nome = nome
        self.cognome = cognome
        self.matricola = matricola
        self.voti = []  # Lista vuota per i voti
    
    def aggiungi_voto(self, voto):
        """
        Aggiunge un voto alla lista se valido (18-30).
        
        Args:
            voto (int): Il voto da aggiungere
        
        Returns:
            bool: True se il voto è stato aggiunto, False altrimenti
        """
        if 18 <= voto <= 30:
            self.voti.append(voto)
            print(f"✅ Voto {voto} aggiunto con successo!")
            return True
        else:
            print(f"❌ Voto {voto} non valido! Deve essere tra 18 e 30.")
            return False
    
    def media_voti(self):
        """
        Calcola la media dei voti.
        
        Returns:
            float: La media dei voti, 0 se non ci sono voti
        """
        if len(self.voti) == 0:
            return 0
        return sum(self.voti) / len(self.voti)
    
    def __str__(self):
        """Rappresentazione testuale dello studente."""
        media = self.media_voti()
        return f"Studente: {self.nome} {self.cognome} (Matricola: {self.matricola}) - Media: {media:.2f}"


# Test della classe
if __name__ == "__main__":
    studente = Studente("Mario", "Rossi", "12345")
    
    studente.aggiungi_voto(28)
    studente.aggiungi_voto(30)
    studente.aggiungi_voto(25)
    studente.aggiungi_voto(15)  # Questo non verrà aggiunto (non valido)
    
    print(studente)
    print(f"Media: {studente.media_voti():.2f}")
2

Classe ContoBancario

⭐⭐ Facile-Medio

📝 Traccia

Crea una classe ContoBancario che simuli un conto corrente bancario con operazioni di deposito, prelievo e visualizzazione del saldo.

✅ Requisiti

  1. Il metodo __init__ deve accettare:
    • titolare (stringa)
    • saldo_iniziale (float, default = 0.0)
  2. Implementa un metodo deposita(importo) che:
    • Aumenta il saldo dell'importo specificato
    • Restituisce True se l'operazione ha successo
    • Restituisce False se l'importo è negativo o zero
  3. Implementa un metodo preleva(importo) che:
    • Diminuisce il saldo dell'importo specificato
    • Restituisce True se l'operazione ha successo
    • Restituisce False se l'importo è negativo/zero o se non ci sono fondi sufficienti
  4. Implementa un metodo get_saldo() che restituisce il saldo corrente
  5. Implementa il metodo __str__ che mostra: "Conto di [titolare] - Saldo: €[saldo]"
Esempio di utilizzo:
conto = ContoBancario("Laura Bianchi", 1000.0)
print(conto)  # Conto di Laura Bianchi - Saldo: €1000.00

conto.deposita(500)   # True
conto.preleva(200)    # True
conto.preleva(2000)   # False (fondi insufficienti)

print(conto)  # Conto di Laura Bianchi - Saldo: €1300.00
                    

💡 Suggerimenti

  • Usa un attributo privato self._saldo per memorizzare il saldo
  • Prima di ogni operazione, controlla la validità dell'importo
  • Nel prelievo, controlla che ci siano fondi sufficienti: if self._saldo >= importo:
  • Usa la formattazione {saldo:.2f} per mostrare 2 decimali

✅ Soluzione Proposta

class ContoBancario:
    """Rappresenta un conto corrente bancario."""
    
    def __init__(self, titolare, saldo_iniziale=0.0):
        """
        Inizializza un conto bancario.
        
        Args:
            titolare (str): Nome del titolare del conto
            saldo_iniziale (float): Saldo iniziale del conto (default: 0.0)
        """
        self.titolare = titolare
        self._saldo = saldo_iniziale  # Attributo protetto
    
    def deposita(self, importo):
        """
        Deposita denaro sul conto.
        
        Args:
            importo (float): L'importo da depositare
        
        Returns:
            bool: True se operazione riuscita, False altrimenti
        """
        if importo <= 0:
            print(f"❌ Importo non valido: €{importo:.2f}")
            return False
        
        self._saldo += importo
        print(f"✅ Depositati €{importo:.2f} - Nuovo saldo: €{self._saldo:.2f}")
        return True
    
    def preleva(self, importo):
        """
        Preleva denaro dal conto.
        
        Args:
            importo (float): L'importo da prelevare
        
        Returns:
            bool: True se operazione riuscita, False altrimenti
        """
        if importo <= 0:
            print(f"❌ Importo non valido: €{importo:.2f}")
            return False
        
        if self._saldo < importo:
            print(f"❌ Fondi insufficienti! Saldo: €{self._saldo:.2f}, Richiesto: €{importo:.2f}")
            return False
        
        self._saldo -= importo
        print(f"✅ Prelevati €{importo:.2f} - Nuovo saldo: €{self._saldo:.2f}")
        return True
    
    def get_saldo(self):
        """
        Restituisce il saldo corrente.
        
        Returns:
            float: Il saldo del conto
        """
        return self._saldo
    
    def __str__(self):
        """Rappresentazione testuale del conto."""
        return f"Conto di {self.titolare} - Saldo: €{self._saldo:.2f}"


# Test della classe
if __name__ == "__main__":
    conto = ContoBancario("Laura Bianchi", 1000.0)
    print(conto)
    print()
    
    conto.deposita(500)
    conto.preleva(200)
    conto.preleva(2000)  # Fallirà
    conto.deposita(-100)   # Fallirà
    
    print()
    print(conto)
    print(f"Saldo finale: €{conto.get_saldo():.2f}")
3

Classe Prodotto con Property

⭐⭐⭐ Medio

📝 Traccia

Crea una classe Prodotto che rappresenti un articolo in un negozio. Usa le property per gestire il prezzo e lo sconto con validazione automatica.

✅ Requisiti

  1. Il metodo __init__ deve accettare:
    • nome (stringa)
    • prezzo (float)
    • quantita (int, default = 0)
  2. Crea una property prezzo che:
    • Restituisce il prezzo (getter)
    • Imposta il prezzo solo se è positivo, altrimenti solleva ValueError (setter)
  3. Crea un attributo sconto_percentuale (inizialmente 0) con una property che:
    • Restituisce lo sconto percentuale (getter)
    • Imposta lo sconto solo se è tra 0 e 100 (setter)
  4. Crea una property di sola lettura prezzo_scontato che:
    • Calcola e restituisce il prezzo finale con lo sconto applicato
    • Formula: prezzo * (1 - sconto_percentuale / 100)
  5. Crea una property di sola lettura valore_inventario che:
    • Restituisce il valore totale dell'inventario: prezzo_scontato * quantita
  6. Implementa __str__ per mostrare le informazioni del prodotto
Esempio di utilizzo:
prodotto = Prodotto("Laptop", 1000.0, 5)
print(prodotto)  # Laptop - €1000.00 x 5 = €5000.00

prodotto.sconto_percentuale = 20
print(f"Prezzo scontato: €{prodotto.prezzo_scontato:.2f}")  # €800.00
print(f"Valore inventario: €{prodotto.valore_inventario:.2f}")  # €4000.00

prodotto.prezzo = 1200  # OK
prodotto.prezzo = -50   # Solleva ValueError
                    

💡 Suggerimenti

  • Usa @property per il getter e @nome_property.setter per il setter
  • Gli attributi interni devono iniziare con underscore: self._prezzo
  • Per property di sola lettura, non definire il setter
  • Usa raise ValueError("messaggio") per sollevare eccezioni
  • Ricorda che nelle property il calcolo deve usare self.prezzo, non self._prezzo

✅ Soluzione Proposta

class Prodotto:
    """Rappresenta un prodotto in un negozio con gestione sconto."""
    
    def __init__(self, nome, prezzo, quantita=0):
        """
        Inizializza un prodotto.
        
        Args:
            nome (str): Nome del prodotto
            prezzo (float): Prezzo unitario
            quantita (int): Quantità in magazzino
        """
        self.nome = nome
        self._prezzo = 0  # Inizializziamo a 0
        self.prezzo = prezzo  # Usiamo il setter per validazione
        self.quantita = quantita
        self._sconto_percentuale = 0
    
    # Property per il prezzo
    @property
    def prezzo(self):
        """Restituisce il prezzo del prodotto."""
        return self._prezzo
    
    @prezzo.setter
    def prezzo(self, valore):
        """
        Imposta il prezzo con validazione.
        
        Args:
            valore (float): Il nuovo prezzo
        
        Raises:
            ValueError: Se il prezzo non è positivo
        """
        if valore <= 0:
            raise ValueError(f"Il prezzo deve essere positivo, ricevuto: €{valore:.2f}")
        self._prezzo = valore
    
    # Property per lo sconto
    @property
    def sconto_percentuale(self):
        """Restituisce lo sconto percentuale."""
        return self._sconto_percentuale
    
    @sconto_percentuale.setter
    def sconto_percentuale(self, valore):
        """
        Imposta lo sconto con validazione.
        
        Args:
            valore (float): Lo sconto in percentuale (0-100)
        
        Raises:
            ValueError: Se lo sconto non è tra 0 e 100
        """
        if not 0 <= valore <= 100:
            raise ValueError(f"Lo sconto deve essere tra 0 e 100, ricevuto: {valore}%")
        self._sconto_percentuale = valore
    
    # Property di sola lettura per il prezzo scontato
    @property
    def prezzo_scontato(self):
        """
        Calcola il prezzo con lo sconto applicato.
        
        Returns:
            float: Il prezzo finale dopo lo sconto
        """
        return self.prezzo * (1 - self.sconto_percentuale / 100)
    
    # Property di sola lettura per il valore inventario
    @property
    def valore_inventario(self):
        """
        Calcola il valore totale dell'inventario.
        
        Returns:
            float: prezzo_scontato * quantita
        """
        return self.prezzo_scontato * self.quantita
    
    def __str__(self):
        """Rappresentazione testuale del prodotto."""
        info = f"{self.nome} - €{self.prezzo:.2f}"
        if self.sconto_percentuale > 0:
            info += f" (sconto {self.sconto_percentuale:.0f}% → €{self.prezzo_scontato:.2f})"
        info += f" x {self.quantita} = €{self.valore_inventario:.2f}"
        return info


# Test della classe
if __name__ == "__main__":
    prodotto = Prodotto("Laptop", 1000.0, 5)
    print(prodotto)
    print()
    
    print("Applicando sconto del 20%...")
    prodotto.sconto_percentuale = 20
    print(f"Prezzo scontato: €{prodotto.prezzo_scontato:.2f}")
    print(f"Valore inventario: €{prodotto.valore_inventario:.2f}")
    print(prodotto)
    print()
    
    print("Modificando il prezzo...")
    prodotto.prezzo = 1200
    print(prodotto)
    print()
    
    # Test validazione
    try:
        print("Tentativo di impostare prezzo negativo...")
        prodotto.prezzo = -50
    except ValueError as e:
        print(f"❌ Errore: {e}")
    
    try:
        print("Tentativo di impostare sconto invalido...")
        prodotto.sconto_percentuale = 150
    except ValueError as e:
        print(f"❌ Errore: {e}")
4

Gerarchia di Veicoli

⭐⭐⭐⭐ Medio-Difficile

📝 Traccia

Crea una gerarchia di classi per rappresentare diversi tipi di veicoli. Questo esercizio mette in pratica ereditarietà, polimorfismo e metodi speciali.

✅ Requisiti

  1. Classe base Veicolo:
    • Attributi: marca, modello, anno
    • Metodo descrizione() che restituisce "Marca Modello (Anno)"
    • Metodo emissioni_co2() che solleva NotImplementedError (deve essere implementato dalle sottoclassi)
    • Metodo __str__ che usa descrizione()
  2. Classe Auto (eredita da Veicolo):
    • Attributo aggiuntivo: cilindrata (in litri)
    • Override emissioni_co2(): restituisce cilindrata * 120 g/km
    • Metodo __repr__ per rappresentazione tecnica
  3. Classe Moto (eredita da Veicolo):
    • Attributo aggiuntivo: cilindrata (in cc)
    • Override emissioni_co2(): restituisce cilindrata * 0.08 g/km
  4. Classe VeicoloElettrico (eredita da Veicolo):
    • Attributo aggiuntivo: capacita_batteria (in kWh)
    • Override emissioni_co2(): restituisce 0
    • Property autonomia (sola lettura): capacita_batteria * 5 km
  5. Crea una funzione confronta_emissioni(veicolo1, veicolo2) che:
    • Accetta due veicoli qualsiasi (polimorfismo!)
    • Stampa quale veicolo inquina di meno
Esempio di utilizzo:
auto = Auto("Fiat", "Panda", 2020, 1.2)
moto = Moto("Ducati", "Monster", 2021, 821)
elettrica = VeicoloElettrico("Tesla", "Model 3", 2023, 75)

print(auto)  # Fiat Panda (2020)
print(f"Emissioni: {auto.emissioni_co2():.1f} g/km")  # 144.0 g/km

print(elettrica)
print(f"Emissioni: {elettrica.emissioni_co2():.1f} g/km")  # 0.0 g/km
print(f"Autonomia: {elettrica.autonomia} km")  # 375 km

confronta_emissioni(auto, elettrica)
# La Tesla Model 3 inquina meno della Fiat Panda
                    

💡 Suggerimenti

  • Chiama sempre super().__init__(...) nelle sottoclassi
  • Per NotImplementedError: raise NotImplementedError("messaggio")
  • Il polimorfismo permette di chiamare emissioni_co2() su qualsiasi veicolo
  • Usa type(obj).__name__ per ottenere il nome della classe

✅ Soluzione Proposta

class Veicolo:
    """Classe base per tutti i veicoli."""
    
    def __init__(self, marca, modello, anno):
        """
        Inizializza un veicolo generico.
        
        Args:
            marca (str): Marca del veicolo
            modello (str): Modello del veicolo
            anno (int): Anno di produzione
        """
        self.marca = marca
        self.modello = modello
        self.anno = anno
    
    def descrizione(self):
        """Restituisce una descrizione del veicolo."""
        return f"{self.marca} {self.modello} ({self.anno})"
    
    def emissioni_co2(self):
        """
        Calcola le emissioni di CO2 in g/km.
        Deve essere implementato dalle sottoclassi.
        """
        raise NotImplementedError("Le sottoclassi devono implementare emissioni_co2()")
    
    def __str__(self):
        return self.descrizione()


class Auto(Veicolo):
    """Rappresenta un'automobile."""
    
    def __init__(self, marca, modello, anno, cilindrata):
        """
        Inizializza un'auto.
        
        Args:
            marca (str): Marca dell'auto
            modello (str): Modello dell'auto
            anno (int): Anno di produzione
            cilindrata (float): Cilindrata del motore in litri
        """
        super().__init__(marca, modello, anno)
        self.cilindrata = cilindrata
    
    def emissioni_co2(self):
        """
        Calcola le emissioni di CO2.
        
        Returns:
            float: Emissioni in g/km (cilindrata * 120)
        """
        return self.cilindrata * 120
    
    def __repr__(self):
        return f"Auto(marca='{self.marca}', modello='{self.modello}', anno={self.anno}, cilindrata={self.cilindrata})"


class Moto(Veicolo):
    """Rappresenta una motocicletta."""
    
    def __init__(self, marca, modello, anno, cilindrata):
        """
        Inizializza una moto.
        
        Args:
            marca (str): Marca della moto
            modello (str): Modello della moto
            anno (int): Anno di produzione
            cilindrata (int): Cilindrata del motore in cc
        """
        super().__init__(marca, modello, anno)
        self.cilindrata = cilindrata
    
    def emissioni_co2(self):
        """
        Calcola le emissioni di CO2.
        
        Returns:
            float: Emissioni in g/km (cilindrata * 0.08)
        """
        return self.cilindrata * 0.08


class VeicoloElettrico(Veicolo):
    """Rappresenta un veicolo elettrico."""
    
    def __init__(self, marca, modello, anno, capacita_batteria):
        """
        Inizializza un veicolo elettrico.
        
        Args:
            marca (str): Marca del veicolo
            modello (str): Modello del veicolo
            anno (int): Anno di produzione
            capacita_batteria (float): Capacità della batteria in kWh
        """
        super().__init__(marca, modello, anno)
        self.capacita_batteria = capacita_batteria
    
    def emissioni_co2(self):
        """
        I veicoli elettrici non hanno emissioni dirette.
        
        Returns:
            float: 0 g/km
        """
        return 0
    
    @property
    def autonomia(self):
        """
        Calcola l'autonomia stimata.
        
        Returns:
            float: Autonomia in km (capacita_batteria * 5)
        """
        return self.capacita_batteria * 5


def confronta_emissioni(veicolo1, veicolo2):
    """
    Confronta le emissioni di CO2 di due veicoli.
    Dimostra il polimorfismo!
    
    Args:
        veicolo1 (Veicolo): Primo veicolo
        veicolo2 (Veicolo): Secondo veicolo
    """
    em1 = veicolo1.emissioni_co2()
    em2 = veicolo2.emissioni_co2()
    
    print(f"
🔍 Confronto Emissioni:")
    print(f"   {veicolo1.descrizione()}: {em1:.1f} g/km CO2")
    print(f"   {veicolo2.descrizione()}: {em2:.1f} g/km CO2")
    
    if em1 < em2:
        print(f"✅ {veicolo1.descrizione()} inquina meno!")
    elif em2 < em1:
        print(f"✅ {veicolo2.descrizione()} inquina meno!")
    else:
        print("⚖️  Entrambi hanno le stesse emissioni!")


# Test delle classi
if __name__ == "__main__":
    auto = Auto("Fiat", "Panda", 2020, 1.2)
    moto = Moto("Ducati", "Monster", 2021, 821)
    elettrica = VeicoloElettrico("Tesla", "Model 3", 2023, 75)
    
    print("🚗 === AUTO ===")
    print(auto)
    print(f"Emissioni: {auto.emissioni_co2():.1f} g/km")
    print(f"Repr: {repr(auto)}")
    
    print("
🏍️  === MOTO ===")
    print(moto)
    print(f"Emissioni: {moto.emissioni_co2():.1f} g/km")
    
    print("
⚡ === VEICOLO ELETTRICO ===")
    print(elettrica)
    print(f"Emissioni: {elettrica.emissioni_co2():.1f} g/km")
    print(f"Autonomia: {elettrica.autonomia:.0f} km")
    
    # Polimorfismo in azione!
    confronta_emissioni(auto, elettrica)
    confronta_emissioni(moto, auto)
    
    # Lista polimorfica
    print("
📊 === TUTTI I VEICOLI ===")
    veicoli = [auto, moto, elettrica]
    for v in veicoli:
        print(f"   {v}{v.emissioni_co2():.1f} g/km")
5

Sistema Gestione Playlist Musicale

⭐⭐⭐⭐⭐ Difficile

📝 Traccia

Crea un sistema completo per gestire playlist musicali. Questo esercizio integra tutti i concetti visti nella lezione: classi, ereditarietà, polimorfismo, property, metodi speciali, composizione e incapsulamento.

✅ Requisiti

  1. Classe Brano:
    • Attributi: titolo, artista, durata_secondi
    • Property durata_formattata (sola lettura): restituisce "MM:SS"
    • Metodo __str__: "Titolo - Artista (MM:SS)"
    • Metodo __repr__ per debugging
    • Metodi __eq__ e __lt__ per confronti (confronta per durata)
  2. Classe Playlist:
    • Attributi: nome, lista di brani (inizialmente vuota)
    • Metodo aggiungi_brano(brano): aggiunge un brano
    • Metodo rimuovi_brano(brano): rimuove un brano
    • Property durata_totale (sola lettura): somma durate di tutti i brani
    • Property durata_totale_formattata (sola lettura): formato "HH:MM:SS"
    • Metodo __len__: restituisce il numero di brani
    • Metodo __getitem__: permette accesso con indice playlist[i]
    • Metodo __contains__: permette brano in playlist
    • Metodo ordina_per_durata(): ordina i brani per durata
    • Metodo stampa_playlist(): mostra tutti i brani numerati
  3. Classe PlaylistSmart (eredita da Playlist):
    • Override aggiungi_brano: non permette duplicati
    • Metodo shuffle(): mescola l'ordine dei brani
    • Property brano_piu_lungo (sola lettura): restituisce il brano più lungo
Esempio di utilizzo:
# Creiamo alcuni brani
brano1 = Brano("Bohemian Rhapsody", "Queen", 354)
brano2 = Brano("Stairway to Heaven", "Led Zeppelin", 482)
brano3 = Brano("Hotel California", "Eagles", 391)

# Creiamo una playlist
playlist = PlaylistSmart("Rock Classics")
playlist.aggiungi_brano(brano1)
playlist.aggiungi_brano(brano2)
playlist.aggiungi_brano(brano3)

print(f"Brani nella playlist: {len(playlist)}")  # 3
print(f"Durata totale: {playlist.durata_totale_formattata}")  # 00:20:27

playlist.stampa_playlist()
print(f"Il brano più lungo è: {playlist.brano_piu_lungo}")
                    

💡 Suggerimenti

  • Per formattare MM:SS: f"{minuti:02d}:{secondi:02d}"
  • Per HH:MM:SS calcola ore, poi minuti, poi secondi rimanenti
  • __eq__ confronta titolo E artista per uguaglianza
  • __lt__ confronta la durata: self.durata_secondi < other.durata_secondi
  • Per shuffle usa import random e random.shuffle(lista)
  • Per trovare il brano più lungo usa max(self.brani, key=lambda b: b.durata_secondi)
  • In PlaylistSmart controlla duplicati con if brano not in self.brani:

✅ Soluzione Proposta

import random

class Brano:
    """Rappresenta un brano musicale."""
    
    def __init__(self, titolo, artista, durata_secondi):
        """
        Inizializza un brano.
        
        Args:
            titolo (str): Titolo del brano
            artista (str): Nome dell'artista
            durata_secondi (int): Durata in secondi
        """
        self.titolo = titolo
        self.artista = artista
        self.durata_secondi = durata_secondi
    
    @property
    def durata_formattata(self):
        """
        Restituisce la durata in formato MM:SS.
        
        Returns:
            str: Durata formattata
        """
        minuti = self.durata_secondi // 60
        secondi = self.durata_secondi % 60
        return f"{minuti:02d}:{secondi:02d}"
    
    def __str__(self):
        """Rappresentazione user-friendly."""
        return f"{self.titolo} - {self.artista} ({self.durata_formattata})"
    
    def __repr__(self):
        """Rappresentazione tecnica."""
        return f"Brano(titolo='{self.titolo}', artista='{self.artista}', durata={self.durata_secondi}s)"
    
    def __eq__(self, other):
        """Due brani sono uguali se hanno stesso titolo e artista."""
        if not isinstance(other, Brano):
            return False
        return self.titolo == other.titolo and self.artista == other.artista
    
    def __lt__(self, other):
        """Confronta brani per durata (per ordinamento)."""
        return self.durata_secondi < other.durata_secondi


class Playlist:
    """Gestisce una playlist di brani musicali."""
    
    def __init__(self, nome):
        """
        Inizializza una playlist.
        
        Args:
            nome (str): Nome della playlist
        """
        self.nome = nome
        self.brani = []  # Lista di oggetti Brano
    
    def aggiungi_brano(self, brano):
        """
        Aggiunge un brano alla playlist.
        
        Args:
            brano (Brano): Il brano da aggiungere
        """
        self.brani.append(brano)
        print(f"✅ Aggiunto: {brano.titolo}")
    
    def rimuovi_brano(self, brano):
        """
        Rimuove un brano dalla playlist.
        
        Args:
            brano (Brano): Il brano da rimuovere
        
        Returns:
            bool: True se rimosso, False se non trovato
        """
        if brano in self.brani:
            self.brani.remove(brano)
            print(f"❌ Rimosso: {brano.titolo}")
            return True
        print(f"⚠️  Brano non trovato: {brano.titolo}")
        return False
    
    @property
    def durata_totale(self):
        """
        Calcola la durata totale della playlist in secondi.
        
        Returns:
            int: Durata totale in secondi
        """
        return sum(brano.durata_secondi for brano in self.brani)
    
    @property
    def durata_totale_formattata(self):
        """
        Restituisce la durata totale in formato HH:MM:SS.
        
        Returns:
            str: Durata formattata
        """
        totale = self.durata_totale
        ore = totale // 3600
        minuti = (totale % 3600) // 60
        secondi = totale % 60
        return f"{ore:02d}:{minuti:02d}:{secondi:02d}"
    
    def __len__(self):
        """Restituisce il numero di brani nella playlist."""
        return len(self.brani)
    
    def __getitem__(self, index):
        """Permette l'accesso ai brani tramite indice."""
        return self.brani[index]
    
    def __contains__(self, brano):
        """Permette di usare 'in' per controllare se un brano è nella playlist."""
        return brano in self.brani
    
    def ordina_per_durata(self, reverse=False):
        """
        Ordina i brani per durata.
        
        Args:
            reverse (bool): Se True ordina dal più lungo al più corto
        """
        self.brani.sort(reverse=reverse)
        print(f"🔄 Playlist ordinata per durata ({'decrescente' if reverse else 'crescente'})")
    
    def stampa_playlist(self):
        """Stampa tutti i brani della playlist."""
        print(f"
🎵 === {self.nome.upper()} ===")
        print(f"Brani: {len(self)} | Durata totale: {self.durata_totale_formattata}")
        print("-" * 50)
        for i, brano in enumerate(self.brani, 1):
            print(f"{i:2d}. {brano}")


class PlaylistSmart(Playlist):
    """Playlist con funzionalità avanzate."""
    
    def aggiungi_brano(self, brano):
        """
        Override: non permette duplicati.
        
        Args:
            brano (Brano): Il brano da aggiungere
        
        Returns:
            bool: True se aggiunto, False se duplicato
        """
        if brano in self.brani:
            print(f"⚠️  Brano già presente: {brano.titolo}")
            return False
        super().aggiungi_brano(brano)  # Chiama il metodo della classe base
        return True
    
    def shuffle(self):
        """Mescola casualmente l'ordine dei brani."""
        random.shuffle(self.brani)
        print("🔀 Playlist mescolata casualmente!")
    
    @property
    def brano_piu_lungo(self):
        """
        Restituisce il brano più lungo della playlist.
        
        Returns:
            Brano: Il brano con durata maggiore, None se playlist vuota
        """
        if not self.brani:
            return None
        return max(self.brani, key=lambda b: b.durata_secondi)


# ========================================
# TEST COMPLETO DEL SISTEMA
# ========================================

if __name__ == "__main__":
    # Creiamo alcuni brani
    print("🎸 Creazione brani...")
    brano1 = Brano("Bohemian Rhapsody", "Queen", 354)
    brano2 = Brano("Stairway to Heaven", "Led Zeppelin", 482)
    brano3 = Brano("Hotel California", "Eagles", 391)
    brano4 = Brano("Comfortably Numb", "Pink Floyd", 382)
    brano5 = Brano("Sweet Child O' Mine", "Guns N' Roses", 356)
    
    # Creiamo una playlist smart
    print("
📝 Creazione playlist...")
    playlist = PlaylistSmart("Rock Classics")
    
    # Aggiungiamo brani
    print("
➕ Aggiunta brani...")
    playlist.aggiungi_brano(brano1)
    playlist.aggiungi_brano(brano2)
    playlist.aggiungi_brano(brano3)
    playlist.aggiungi_brano(brano4)
    playlist.aggiungi_brano(brano5)
    
    # Test duplicati
    print("
🔁 Test duplicati...")
    playlist.aggiungi_brano(brano1)  # Non verrà aggiunto
    
    # Mostriamo la playlist
    playlist.stampa_playlist()
    
    # Test operatori speciali
    print("
🔍 Test operatori speciali...")
    print(f"Numero di brani: {len(playlist)}")
    print(f"Primo brano: {playlist[0]}")
    print(f"Bohemian Rhapsody è nella playlist? {brano1 in playlist}")
    
    # Brano più lungo
    print("
⏱️  Brano più lungo...")
    print(f"Il brano più lungo è: {playlist.brano_piu_lungo}")
    
    # Ordiniamo
    print("
📊 Ordinamento...")
    playlist.ordina_per_durata(reverse=True)
    playlist.stampa_playlist()
    
    # Shuffle
    print("
🎲 Mescolamento casuale...")
    playlist.shuffle()
    playlist.stampa_playlist()
    
    # Rimozione
    print("
➖ Rimozione brano...")
    playlist.rimuovi_brano(brano3)
    playlist.stampa_playlist()
    
    print("
✅ Test completato!")

🎓 Conclusione

Complimenti per aver completato l'esercitazione! Se sei riuscito a risolvere tutti e 5 gli esercizi, hai dimostrato di aver compreso i concetti fondamentali della Programmazione Orientata agli Oggetti in Python.

✅ Cosa Hai Imparato

  • Esercizio 1: Classi base, __init__, metodi semplici
  • Esercizio 2: Incapsulamento, validazione input, getter
  • Esercizio 3: Property avanzate, validazione con setter, property calcolate
  • Esercizio 4: Ereditarietà, polimorfismo, metodi astratti, super()
  • Esercizio 5: Integrazione completa di tutti i concetti, metodi speciali avanzati

🚀 Prossimi Passi

Ora che hai padroneggiato le basi, prova a:

  • Estendere questi esercizi con funzionalità aggiuntive
  • Creare progetti personali usando la OOP
  • Studiare design patterns come Singleton, Factory, Observer
  • Esplorare concetti avanzati come decoratori e metaclassi